package zone

import (
	"context"
	"errors"
	"fmt"
	"github.com/Unixeno/TheMoon/logging"
	"github.com/Unixeno/TheMoon/moon/config"
	"github.com/Unixeno/TheMoon/moon/zone/network"
	zf "github.com/Unixeno/TheMoon/zonefile"
	"github.com/Unixeno/islands/pb"
	"github.com/golang/protobuf/ptypes/empty"
	"google.golang.org/grpc"
	"net"
	"regexp"
)

const (
	stateRunning         = "Running"
	stateCreated         = "Created"
	stateExited          = "Exited"
	stateRemoving        = "Removing"
	stateDead            = "Dead"
	stateRestarting      = "Restarting"
	statePaused          = "Paused"
	supportedVersion     = "0.1"
	apiPattern           = "(tcp|unix)://(.+)"
	defaultTimeout       = 5
	defaultWaitCondition = "not-running"
)

var islandsClient pb.IslandsClient

func initIslands(ctx context.Context) {
	logger.WithField("path", config.SysConfig.Zone.ApiPath).Info("connect to islands")
	reg := regexp.MustCompile(apiPattern)
	r := reg.FindStringSubmatch(config.SysConfig.Zone.ApiPath)
	if r == nil || len(r) != 3 {
		logger.Fatal("invalid islands APIPath")
	}

	dialer := grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) {
		return net.Dial(r[1], r[2])
	})

	if conn, err := grpc.Dial(config.SysConfig.Zone.ApiPath, grpc.WithInsecure(), dialer); err != nil {
		logger.WithError(err).Fatal("failed to connect to islands service")
	} else {
		islandsClient = pb.NewIslandsClient(conn)
	}
	checkVersion(ctx)
}

func checkVersion(ctx context.Context) {
	version, err := islandsClient.Version(ctx, &empty.Empty{})
	if err != nil {
		logger.WithError(err).Fatal("failed to validate islands api version")
	} else {
		if version.ApiVersion != supportedVersion {
			logger.WithField("supported", supportedVersion).WithField("remote", version.ApiVersion).Fatal("unsupported islands api version")
		}
	}
	logger.WithFields(logging.Fields{
		"islands_version":     version.Version,
		"islands_api_version": version.ApiVersion,
		"git":                 version.GitCommit,
		"built":               version.Built,
	}).Info()
}

var errImageNotExist = errors.New("image doesn't exist")
var errContainerNotExist = errors.New("container doesn't exist")
var errVolumeNotExist = errors.New("volume doesn't exist")
var errNetworkExist = errors.New("network exist")

//
func (d *deployer) isVolumeExist(ctx context.Context, name string) bool {
	resp, err := islandsClient.ListVolume(ctx, &empty.Empty{})
	if err != nil {
		return false
	}

	for _, volume := range resp.Volumes {
		if volume.Name == name {
			return true
		}
	}

	return false
}

func (d *deployer) getVolumeHostPath(ctx context.Context, name string) (string, bool) {
	resp, err := islandsClient.ListVolume(ctx, &empty.Empty{})
	if err != nil {
		return "", false
	}

	for _, volume := range resp.Volumes {
		if volume.Name == name {
			return volume.Path, true
		}
	}

	return "", false

}

func (d *deployer) createVolume(ctx context.Context, volume *zf.Volume) error {
	name := d.volumes[volume.Name]
	if volume.Source == "" {
		req := pb.CreateVolumeReq{
			Name: name,
		}
		if _, err := islandsClient.CreateVolume(ctx, &req); err != nil {
			return err
		}
		return nil
	}
	req := pb.AddVolumeReq{
		Path: volume.Source,
		Name: name,
	}
	_, err := islandsClient.AddVolume(ctx, &req)
	if err != nil {
		return err
	}

	return nil

}

func (d *deployer) removeVolume(ctx context.Context, name string) error {
	if !d.isVolumeExist(ctx, name) {
		return errVolumeNotExist
	}
	_, err := islandsClient.DeleteVolume(ctx, &pb.DeleteVolumeReq{Name: name})

	// 删除卷
	for sourceName, currentName := range d.volumes {
		if currentName == name {
			delete(d.volumes, sourceName)
			break
		}
	}

	// 考虑在回收的时候也要调用这个函数所以在 removeVolume 保存这个 deployer
	d.save()
	return err
}

func (d *deployer) isImageExist(ctx context.Context, id string) bool {
	resp, err := islandsClient.ListImage(ctx, &empty.Empty{})
	if err != nil {
		return false
	}

	for _, image := range resp.Images {
		if image.Id == id {
			return true
		}
	}

	return false
}

func (d *deployer) getImageId(ctx context.Context, name string) (string, error) {
	resp, err := islandsClient.ListImage(ctx, &empty.Empty{})
	if err != nil {
		return "", err
	}

	for _, image := range resp.Images {
		if image.Name == name {
			return image.Id, nil
		}
	}

	return "", errImageNotExist
}

func (d *deployer) getContainerId(ctx context.Context, name string) (string, error) {
	resp, err := islandsClient.ListContainer(ctx, &empty.Empty{})
	if err != nil {
		return "", err
	}
	for _, container := range resp.Containers {
		if container.Name == name {
			return container.Id, nil
		}
	}

	return "", errContainerNotExist
}

func (d *deployer) getContainerInfo(ctx context.Context, id string) (*pb.Container, error) {
	resp, err := islandsClient.InspectContainer(ctx, &pb.ContainerID{Id: id})
	if err != nil {
		return nil, err
	}

	return resp.Info, nil
}

func (d *deployer) getContainerState(ctx context.Context, id string) (string, error) {
	info, err := d.getContainerInfo(ctx, id)
	if err != nil {
		return "", nil
	}
	return info.State.State, nil
}

func (d *deployer) checkOrPullImage(ctx context.Context, name string) (string, error) {
	imageId, err := d.getImageId(ctx, name)
	if err != nil && err != errImageNotExist {
		return "", err
	}

	if imageId == "" {
		if err := d.imagePull(ctx, name); err != nil {
			return "", err
		}
	}

	return d.getImageId(ctx, name)
}

func (d *deployer) stopContainer(ctx context.Context, id string) error {
	stopReq := pb.StopContainerReq{
		Id:      id,
		Timeout: defaultTimeout,
	}
	_, err := islandsClient.StopContainer(ctx, &stopReq)
	return err
}

func (d *deployer) removeContainer(ctx context.Context, id string) error {
	removeReq := pb.ContainerID{
		Id: id,
	}
	_, err := islandsClient.RemoveContainer(ctx, &removeReq)
	return err
}

func (d *deployer) waitContainer(ctx context.Context, id string) error {

	resp, err := islandsClient.WaitContainer(ctx, &pb.WaitContainerReq{
		Id:        id,
		Condition: defaultWaitCondition,
	})

	if err != nil {
		return err
	}

	info, err := d.getContainerInfo(ctx, id)
	if err != nil {
		return err
	}
	if resp.ExitCode != 0 {
		return fmt.Errorf("container %v doesn't exit normally", info.Name)
	}

	return nil
}

func (d *deployer) removeImage(ctx context.Context, id string) error {
	if !d.isImageExist(ctx, id) {
		return errImageNotExist
	}
	_, err := islandsClient.DeleteImage(ctx, &pb.DeleteImageReq{Id: id})
	return err
}

func (d *deployer) createContainer(ctx context.Context, imageId string, container *zf.Container) (string, error) {
	name := d.getPrefix() + container.Name
	id, _ := d.getContainerId(ctx, name)

	// 重启的时候不需要重新创建容器
	if id != "" {
		return id, nil
	}

	// 保证卷和网络的创建在容器创建之前
	volumes := make([]*pb.ContainerVolume, len(container.Volumes))
	i := 0
	for name, path := range container.Volumes {
		volumes[i] = &pb.ContainerVolume{
			Name:        d.volumes[name],
			Destination: path,
		}
	}

	interfaces := make([]*pb.NetworkInterface, len(container.Interface))

	for i, n := range container.Interface {
		bridgeName := d.getBridgeName(n.Network)
		logger.Debug("bridgeName ", bridgeName)
		bridge := network.GetNetwork(bridgeName)
		containerIp, gateway, err := bridge.Allocate()
		if err != nil {
			return "", err
		}
		logger.WithField("gateway", gateway.String()).WithField("containerIP", containerIp.String()).Debug()
		mask, _ := bridge.Addr().Mask.Size()
		interfaces[i] = &pb.NetworkInterface{
			Name:    n.Name,
			Bridge:  bridgeName,
			Gateway: gateway.String(),
			Address: []*pb.NetworkAddress{{
				Ip:   containerIp.String(),
				Mask: int32(mask),
			}},
		}
	}

	c := container.PortForwardConfig
	exposedports := map[string]*pb.PortBindings{}
	p1, p2 := c.ContainerPort, c.HostPort
	if p1 != "" {
		hostIP := c.HostIP
		if hostIP == "" {
			hostIP = "0.0.0.0"
		}
		exposedports[p1] = &pb.PortBindings{
			PortBindings: []*pb.PortBinding{
				{
					HostIp:   hostIP,
					HostPort: p2,
				},
			},
		}
	}

	containerNetwork := &pb.Network{
		Hostname:   container.Hostname,
		Dns:        container.DNS,
		Interfaces: interfaces,
	}

	createReq := pb.CreateContainerReq{
		ImageId:      imageId,
		Name:         name,
		Env:          container.Env,
		Tty:          container.Tty,
		Entrypoint:   container.EntryPoint,
		Cmd:          container.CMD,
		WorkingDir:   container.WorkingDir,
		Volumes:      volumes,
		ExposedPorts: exposedports,
		Network:      containerNetwork,
	}

	resp, err := islandsClient.CreateContainer(ctx, &createReq)

	if err != nil {
		return "", err
	}

	return resp.Id, nil
}

func (d *deployer) startContainer(ctx context.Context, id string) error {
	state, err := d.getContainerState(ctx, id)

	if state == stateRunning {
		return nil
	}

	_, err = islandsClient.StartContainer(ctx, &pb.ContainerID{Id: id})
	return err
}

func containerList(ctx context.Context) ([]*pb.ContainerInfo, error) {
	resp, err := islandsClient.ListContainer(ctx, &empty.Empty{})
	if err != nil {
		return nil, err
	}
	return resp.Containers, nil
}
